3 * A handle for managing updates for derived page data on edit, import, purge, etc.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
23 namespace MediaWiki\Storage
;
26 use CategoryMembershipChangeJob
;
34 use InvalidArgumentException
;
37 use LinksDeletionUpdate
;
40 use MediaWiki\Edit\PreparedEdit
;
41 use MediaWiki\Revision\MutableRevisionRecord
;
42 use MediaWiki\Revision\RenderedRevision
;
43 use MediaWiki\Revision\RevisionRecord
;
44 use MediaWiki\Revision\RevisionRenderer
;
45 use MediaWiki\Revision\RevisionSlots
;
46 use MediaWiki\Revision\RevisionStore
;
47 use MediaWiki\Revision\SlotRoleRegistry
;
48 use MediaWiki\Revision\SlotRecord
;
49 use MediaWiki\User\UserIdentity
;
54 use RecentChangesUpdateJob
;
55 use ResourceLoaderWikiModule
;
61 use Wikimedia\Assert\Assert
;
62 use Wikimedia\Rdbms\LBFactory
;
66 * A handle for managing updates for derived page data on edit, import, purge, etc.
68 * @note Avoid direct usage of DerivedPageDataUpdater.
70 * @todo Define interfaces for the different use cases of DerivedPageDataUpdater, particularly
71 * providing access to post-PST content and ParserOutput to callbacks during revision creation,
72 * which currently use WikiPage::prepareContentForEdit, and allowing updates to be triggered on
73 * purge, import, and undeletion, which currently use WikiPage::doEditUpdates() and
74 * Content::getSecondaryDataUpdates().
76 * DerivedPageDataUpdater instances are designed to be cached inside a WikiPage instance,
77 * and re-used by callback code over the course of an update operation. It's a stepping stone
78 * one the way to a more complete refactoring of WikiPage.
80 * When using a DerivedPageDataUpdater, the following life cycle must be observed:
81 * grabCurrentRevision (optional), prepareContent (optional), prepareUpdate (required
82 * for doUpdates). getCanonicalParserOutput, getSlots, and getSecondaryDataUpdates
83 * require prepareContent or prepareUpdate to have been called first, to initialize the
84 * DerivedPageDataUpdater.
86 * @see docs/pageupdater.txt for more information.
88 * MCR migration note: this replaces the relevant methods in WikiPage, and covers the use cases
96 class DerivedPageDataUpdater
implements IDBAccessObject
{
99 * @var UserIdentity|null
101 private $user = null;
111 private $parserCache;
116 private $revisionStore;
126 private $jobQueueGroup;
131 private $messageCache;
136 private $loadbalancerFactory;
139 * @var string see $wgArticleCountMethod
141 private $articleCountMethod;
144 * @var boolean see $wgRCWatchCategoryMembership
146 private $rcWatchCategoryMembership = false;
149 * Stores (most of) the $options parameter of prepareUpdate().
150 * @see prepareUpdate()
157 'oldrevision' => null,
158 'oldcountable' => null,
159 'oldredirect' => null,
160 'triggeringUser' => null,
161 // causeAction/causeAgent default to 'unknown' but that's handled where it's read,
162 // to make the life of prepareUpdate() callers easier.
163 'causeAction' => null,
164 'causeAgent' => null,
168 * The state of the relevant row in page table before the edit.
169 * This is determined by the first call to grabCurrentRevision, prepareContent,
170 * or prepareUpdate (so it is only accessible in 'knows-current' or a later stage).
171 * If pageState was not initialized when prepareUpdate() is called, prepareUpdate() will
172 * attempt to emulate the state of the page table before the edit.
174 * Contains the following fields:
175 * - oldRevision (RevisionRecord|null): the revision that was current before the change
176 * associated with this update. Might not be set, use getParentRevision().
177 * - oldId (int|null): the id of the above revision. 0 if there is no such revision (the change
178 * was about creating a new page); null if not known (that should not happen).
179 * - oldIsRedirect (bool|null): whether the page was a redirect before the change. Lazy-loaded,
180 * can be null; use wasRedirect() instead of direct access.
181 * - oldCountable (bool|null): whether the page was countable before the change (or null
182 * if we don't have that information)
186 private $pageState = null;
189 * @var RevisionSlotsUpdate|null
191 private $slotsUpdate = null;
194 * @var RevisionRecord|null
196 private $parentRevision = null;
199 * @var RevisionRecord|null
201 private $revision = null;
204 * @var RenderedRevision
206 private $renderedRevision = null;
209 * @var RevisionRenderer
211 private $revisionRenderer;
213 /** @var SlotRoleRegistry */
214 private $slotRoleRegistry;
217 * A stage identifier for managing the life cycle of this instance.
218 * Possible stages are 'new', 'knows-current', 'has-content', 'has-revision', and 'done'.
220 * @see docs/pageupdater.txt for documentation of the life cycle.
224 private $stage = 'new';
227 * Transition table for managing the life cycle of DerivedPageDateUpdater instances.
229 * XXX: Overkill. This is a linear order, we could just count. Names are nice though,
230 * and constants are also overkill...
232 * @see docs/pageupdater.txt for documentation of the life cycle.
236 private static $transitions = [
239 'knows-current' => true,
240 'has-content' => true,
241 'has-revision' => true,
244 'knows-current' => true,
245 'has-content' => true,
246 'has-revision' => true,
249 'has-content' => true,
250 'has-revision' => true,
253 'has-revision' => true,
259 * @param WikiPage $wikiPage ,
260 * @param RevisionStore $revisionStore
261 * @param RevisionRenderer $revisionRenderer
262 * @param SlotRoleRegistry $slotRoleRegistry
263 * @param ParserCache $parserCache
264 * @param JobQueueGroup $jobQueueGroup
265 * @param MessageCache $messageCache
266 * @param Language $contLang
267 * @param LBFactory $loadbalancerFactory
269 public function __construct(
271 RevisionStore
$revisionStore,
272 RevisionRenderer
$revisionRenderer,
273 SlotRoleRegistry
$slotRoleRegistry,
274 ParserCache
$parserCache,
275 JobQueueGroup
$jobQueueGroup,
276 MessageCache
$messageCache,
278 LBFactory
$loadbalancerFactory
280 $this->wikiPage
= $wikiPage;
282 $this->parserCache
= $parserCache;
283 $this->revisionStore
= $revisionStore;
284 $this->revisionRenderer
= $revisionRenderer;
285 $this->slotRoleRegistry
= $slotRoleRegistry;
286 $this->jobQueueGroup
= $jobQueueGroup;
287 $this->messageCache
= $messageCache;
288 $this->contLang
= $contLang;
289 // XXX only needed for waiting for replicas to catch up; there should be a narrower
290 // interface for that.
291 $this->loadbalancerFactory
= $loadbalancerFactory;
295 * Transition function for managing the life cycle of this instances.
297 * @see docs/pageupdater.txt for documentation of the life cycle.
299 * @param string $newStage the new stage
300 * @return string the previous stage
302 * @throws LogicException If a transition to the given stage is not possible in the current
305 private function doTransition( $newStage ) {
306 $this->assertTransition( $newStage );
308 $oldStage = $this->stage
;
309 $this->stage
= $newStage;
315 * Asserts that a transition to the given stage is possible, without performing it.
317 * @see docs/pageupdater.txt for documentation of the life cycle.
319 * @param string $newStage the new stage
321 * @throws LogicException If this instance is not in the expected stage
323 private function assertTransition( $newStage ) {
324 if ( empty( self
::$transitions[$this->stage
][$newStage] ) ) {
325 throw new LogicException( "Cannot transition from {$this->stage} to $newStage" );
330 * @return bool|string
332 private function getWikiId() {
333 // TODO: get from RevisionStore
338 * Checks whether this DerivedPageDataUpdater can be re-used for running updates targeting
339 * the given revision.
341 * @param UserIdentity|null $user The user creating the revision in question
342 * @param RevisionRecord|null $revision New revision (after save, if already saved)
343 * @param RevisionSlotsUpdate|null $slotsUpdate New content (before PST)
344 * @param null|int $parentId Parent revision of the edit (use 0 for page creation)
348 public function isReusableFor(
349 UserIdentity
$user = null,
350 RevisionRecord
$revision = null,
351 RevisionSlotsUpdate
$slotsUpdate = null,
356 && $revision->getParentId() !== $parentId
358 throw new InvalidArgumentException( '$parentId should match the parent of $revision' );
361 // NOTE: For null revisions, $user may be different from $this->revision->getUser
362 // and also from $revision->getUser.
363 // But $user should always match $this->user.
364 if ( $user && $this->user
&& $user->getName() !== $this->user
->getName() ) {
368 if ( $revision && $this->revision
&& $this->revision
->getId()
369 && $this->revision
->getId() !== $revision->getId()
374 if ( $this->pageState
376 && $revision->getParentId() !== null
377 && $this->pageState
['oldId'] !== $revision->getParentId()
382 if ( $this->pageState
383 && $parentId !== null
384 && $this->pageState
['oldId'] !== $parentId
389 // NOTE: this check is the primary reason for having the $this->slotsUpdate field!
390 if ( $this->slotsUpdate
392 && !$this->slotsUpdate
->hasSameUpdates( $slotsUpdate )
399 && !$this->revision
->getSlots()->hasSameContent( $revision->getSlots() )
408 * @param string $articleCountMethod "any" or "link".
409 * @see $wgArticleCountMethod
411 public function setArticleCountMethod( $articleCountMethod ) {
412 $this->articleCountMethod
= $articleCountMethod;
416 * @param bool $rcWatchCategoryMembership
417 * @see $wgRCWatchCategoryMembership
419 public function setRcWatchCategoryMembership( $rcWatchCategoryMembership ) {
420 $this->rcWatchCategoryMembership
= $rcWatchCategoryMembership;
426 private function getTitle() {
427 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
428 return $this->wikiPage
->getTitle();
434 private function getWikiPage() {
435 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
436 return $this->wikiPage
;
440 * Determines whether the page being edited already existed.
441 * Only defined after calling grabCurrentRevision() or prepareContent() or prepareUpdate()!
444 * @throws LogicException if called before grabCurrentRevision
446 public function pageExisted() {
447 $this->assertHasPageState( __METHOD__
);
449 return $this->pageState
['oldId'] > 0;
453 * Returns the parent revision of the new revision wrapped by this update.
454 * If the update is a null-edit, this will return the parent of the current (and new) revision.
455 * This will return null if the revision wrapped by this update created the page.
456 * Only defined after calling prepareContent() or prepareUpdate()!
458 * @return RevisionRecord|null the parent revision of the new revision, or null if
459 * the update created the page.
461 private function getParentRevision() {
462 $this->assertPrepared( __METHOD__
);
464 if ( $this->parentRevision
) {
465 return $this->parentRevision
;
468 if ( !$this->pageState
['oldId'] ) {
469 // If there was no current revision, there is no parent revision,
470 // since the page didn't exist.
474 $oldId = $this->revision
->getParentId();
475 $flags = $this->useMaster() ? RevisionStore
::READ_LATEST
: 0;
476 $this->parentRevision
= $oldId
477 ?
$this->revisionStore
->getRevisionById( $oldId, $flags )
480 return $this->parentRevision
;
484 * Returns the revision that was the page's current revision when grabCurrentRevision()
487 * During an edit, that revision will act as the logical parent of the new revision.
489 * Some updates are performed based on the difference between the database state at the
490 * moment this method is first called, and the state after the edit.
492 * @see docs/pageupdater.txt for more information on when thie method can and should be called.
494 * @note After prepareUpdate() was called, grabCurrentRevision() will throw an exception
495 * to avoid confusion, since the page's current revision is then the new revision after
496 * the edit, which was presumably passed to prepareUpdate() as the $revision parameter.
497 * Use getParentRevision() instead to access the revision that is the parent of the
500 * @return RevisionRecord|null the page's current revision, or null if the page does not
503 public function grabCurrentRevision() {
504 if ( $this->pageState
) {
505 return $this->pageState
['oldRevision'];
508 $this->assertTransition( 'knows-current' );
510 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
511 $wikiPage = $this->getWikiPage();
513 // Do not call WikiPage::clear(), since the caller may already have caused page data
514 // to be loaded with SELECT FOR UPDATE. Just assert it's loaded now.
515 $wikiPage->loadPageData( self
::READ_LATEST
);
516 $rev = $wikiPage->getRevision();
517 $current = $rev ?
$rev->getRevisionRecord() : null;
520 'oldRevision' => $current,
521 'oldId' => $rev ?
$rev->getId() : 0,
522 'oldIsRedirect' => $wikiPage->isRedirect(), // NOTE: uses page table
523 'oldCountable' => $wikiPage->isCountable(), // NOTE: uses pagelinks table
526 $this->doTransition( 'knows-current' );
528 return $this->pageState
['oldRevision'];
532 * Whether prepareUpdate() or prepareContent() have been called on this instance.
536 public function isContentPrepared() {
537 return $this->revision
!== null;
541 * Whether prepareUpdate() has been called on this instance.
543 * @note will also return null in case of a null-edit!
547 public function isUpdatePrepared() {
548 return $this->revision
!== null && $this->revision
->getId() !== null;
554 private function getPageId() {
555 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
556 return $this->wikiPage
->getId();
560 * Whether the content is deleted and thus not visible to the public.
564 public function isContentDeleted() {
565 if ( $this->revision
) {
566 // XXX: if that revision is the current revision, this should be skipped
567 return $this->revision
->isDeleted( RevisionRecord
::DELETED_TEXT
);
569 // If the content has not been saved yet, it cannot have been deleted yet.
575 * Returns the slot, modified or inherited, after PST, with no audience checks applied.
577 * @param string $role slot role name
579 * @throws PageUpdateException If the slot is neither set for update nor inherited from the
583 public function getRawSlot( $role ) {
584 return $this->getSlots()->getSlot( $role );
588 * Returns the content of the given slot, with no audience checks.
590 * @throws PageUpdateException If the slot is neither set for update nor inherited from the
592 * @param string $role slot role name
595 public function getRawContent( $role ) {
596 return $this->getRawSlot( $role )->getContent();
600 * Returns the content model of the given slot
602 * @param string $role slot role name
605 private function getContentModel( $role ) {
606 return $this->getRawSlot( $role )->getModel();
610 * @param string $role slot role name
611 * @return ContentHandler
613 private function getContentHandler( $role ) {
614 // TODO: inject something like a ContentHandlerRegistry
615 return ContentHandler
::getForModelID( $this->getContentModel( $role ) );
618 private function useMaster() {
619 // TODO: can we just set a flag to true in prepareContent()?
620 return $this->wikiPage
->wasLoadedFrom( self
::READ_LATEST
);
626 public function isCountable() {
627 // NOTE: Keep in sync with WikiPage::isCountable.
629 if ( !$this->getTitle()->isContentPage() ) {
633 if ( $this->isContentDeleted() ) {
634 // This should be irrelevant: countability only applies to the current revision,
635 // and the current revision is never suppressed.
639 if ( $this->isRedirect() ) {
645 if ( $this->articleCountMethod
=== 'link' ) {
646 // NOTE: it would be more appropriate to determine for each slot separately
647 // whether it has links, and use that information with that slot's
648 // isCountable() method. However, that would break parity with
649 // WikiPage::isCountable, which uses the pagelinks table to determine
650 // whether the current revision has links.
651 $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() );
654 foreach ( $this->getModifiedSlotRoles() as $role ) {
655 $roleHandler = $this->slotRoleRegistry
->getRoleHandler( $role );
656 if ( $roleHandler->supportsArticleCount() ) {
657 $content = $this->getRawContent( $role );
659 if ( $content->isCountable( $hasLinks ) ) {
671 public function isRedirect() {
672 // NOTE: main slot determines redirect status
673 // TODO: MCR: this should be controlled by a PageTypeHandler
674 $mainContent = $this->getRawContent( SlotRecord
::MAIN
);
676 return $mainContent->isRedirect();
680 * @param RevisionRecord $rev
684 private function revisionIsRedirect( RevisionRecord
$rev ) {
685 // NOTE: main slot determines redirect status
686 $mainContent = $rev->getContent( SlotRecord
::MAIN
, RevisionRecord
::RAW
);
688 return $mainContent->isRedirect();
692 * Prepare updates based on an update which has not yet been saved.
694 * This may be used to create derived data that is needed when creating a new revision;
695 * particularly, this makes available the slots of the new revision via the getSlots()
696 * method, after applying PST and slot inheritance.
698 * The derived data prepared for revision creation may then later be re-used by doUpdates(),
699 * without the need to re-calculate.
701 * @see docs/pageupdater.txt for more information on when thie method can and should be called.
703 * @note Calling this method more than once with the same $slotsUpdate
704 * has no effect. Calling this method multiple times with different content will cause
707 * @note Calling this method after prepareUpdate() has been called will cause an exception.
709 * @param User $user The user to act as context for pre-save transformation (PST).
710 * Type hint should be reduced to UserIdentity at some point.
711 * @param RevisionSlotsUpdate $slotsUpdate The new content of the slots to be updated
712 * by this edit, before PST.
713 * @param bool $useStash Whether to use stashed ParserOutput
715 public function prepareContent(
717 RevisionSlotsUpdate
$slotsUpdate,
720 if ( $this->slotsUpdate
) {
721 if ( !$this->user
) {
722 throw new LogicException(
723 'Unexpected state: $this->slotsUpdate was initialized, '
724 . 'but $this->user was not.'
728 if ( $this->user
->getName() !== $user->getName() ) {
729 throw new LogicException( 'Can\'t call prepareContent() again for different user! '
730 . 'Expected ' . $this->user
->getName() . ', got ' . $user->getName()
734 if ( !$this->slotsUpdate
->hasSameUpdates( $slotsUpdate ) ) {
735 throw new LogicException(
736 'Can\'t call prepareContent() again with different slot content!'
740 return; // prepareContent() already done, nothing to do
743 $this->assertTransition( 'has-content' );
745 $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
746 $title = $this->getTitle();
748 $parentRevision = $this->grabCurrentRevision();
750 $this->slotsOutput
= [];
751 $this->canonicalParserOutput
= null;
753 // The edit may have already been prepared via api.php?action=stashedit
754 $stashedEdit = false;
756 // TODO: MCR: allow output for all slots to be stashed.
757 if ( $useStash && $slotsUpdate->isModifiedSlot( SlotRecord
::MAIN
) ) {
758 $mainContent = $slotsUpdate->getModifiedSlot( SlotRecord
::MAIN
)->getContent();
759 $legacyUser = User
::newFromIdentity( $user );
760 $stashedEdit = ApiStashEdit
::checkCache( $title, $mainContent, $legacyUser );
763 $userPopts = ParserOptions
::newFromUserAndLang( $user, $this->contLang
);
764 Hooks
::run( 'ArticlePrepareTextForEdit', [ $wikiPage, $userPopts ] );
767 $this->slotsUpdate
= $slotsUpdate;
769 if ( $parentRevision ) {
770 $this->revision
= MutableRevisionRecord
::newFromParentRevision( $parentRevision );
772 $this->revision
= new MutableRevisionRecord( $title );
775 // NOTE: user and timestamp must be set, so they can be used for
776 // {{subst:REVISIONUSER}} and {{subst:REVISIONTIMESTAMP}} in PST!
777 $this->revision
->setTimestamp( wfTimestampNow() );
778 $this->revision
->setUser( $user );
780 // Set up ParserOptions to operate on the new revision
781 $oldCallback = $userPopts->getCurrentRevisionCallback();
782 $userPopts->setCurrentRevisionCallback(
783 function ( Title
$parserTitle, $parser = false ) use ( $title, $oldCallback ) {
784 if ( $parserTitle->equals( $title ) ) {
785 $legacyRevision = new Revision( $this->revision
);
786 return $legacyRevision;
788 return call_user_func( $oldCallback, $parserTitle, $parser );
793 $pstContentSlots = $this->revision
->getSlots();
795 foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
796 $slot = $slotsUpdate->getModifiedSlot( $role );
798 if ( $slot->isInherited() ) {
799 // No PST for inherited slots! Note that "modified" slots may still be inherited
800 // from an earlier version, e.g. for rollbacks.
802 } elseif ( $role === SlotRecord
::MAIN
&& $stashedEdit ) {
803 // TODO: MCR: allow PST content for all slots to be stashed.
804 $pstSlot = SlotRecord
::newUnsaved( $role, $stashedEdit->pstContent
);
806 $content = $slot->getContent();
807 $pstContent = $content->preSaveTransform( $title, $this->user
, $userPopts );
808 $pstSlot = SlotRecord
::newUnsaved( $role, $pstContent );
811 $pstContentSlots->setSlot( $pstSlot );
814 foreach ( $slotsUpdate->getRemovedRoles() as $role ) {
815 $pstContentSlots->removeSlot( $role );
818 $this->options
['created'] = ( $parentRevision === null );
819 $this->options
['changed'] = ( $parentRevision === null
820 ||
!$pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
822 $this->doTransition( 'has-content' );
824 if ( !$this->options
['changed'] ) {
827 // TODO: move this into MutableRevisionRecord
828 // TODO: This needs to behave differently for a forced dummy edit!
829 $this->revision
->setId( $parentRevision->getId() );
830 $this->revision
->setTimestamp( $parentRevision->getTimestamp() );
831 $this->revision
->setPageId( $parentRevision->getPageId() );
832 $this->revision
->setParentId( $parentRevision->getParentId() );
833 $this->revision
->setUser( $parentRevision->getUser( RevisionRecord
::RAW
) );
834 $this->revision
->setComment( $parentRevision->getComment( RevisionRecord
::RAW
) );
835 $this->revision
->setMinorEdit( $parentRevision->isMinor() );
836 $this->revision
->setVisibility( $parentRevision->getVisibility() );
838 // prepareUpdate() is redundant for null-edits
839 $this->doTransition( 'has-revision' );
841 $this->parentRevision
= $parentRevision;
844 $renderHints = [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord
::RAW
];
846 if ( $stashedEdit ) {
847 /** @var ParserOutput $output */
848 $output = $stashedEdit->output
;
850 // TODO: this should happen when stashing the ParserOutput, not now!
851 $output->setCacheTime( $stashedEdit->timestamp
);
853 $renderHints['known-revision-output'] = $output;
856 // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
857 // NOTE: the revision is either new or current, so we can bypass audience checks.
858 $this->renderedRevision
= $this->revisionRenderer
->getRenderedRevision(
867 * Returns the update's target revision - that is, the revision that will be the current
868 * revision after the update.
870 * @note Callers must treat the returned RevisionRecord's content as immutable, even
871 * if it is a MutableRevisionRecord instance. Other aspects of a MutableRevisionRecord
872 * returned from here, such as the user or the comment, may be changed, but may not
873 * be reflected in ParserOutput until after prepareUpdate() has been called.
875 * @todo This is currently used by PageUpdater::makeNewRevision() to construct an unsaved
876 * MutableRevisionRecord instance. Introduce something like an UnsavedRevisionFactory service
877 * for that purpose instead!
879 * @return RevisionRecord
881 public function getRevision() {
882 $this->assertPrepared( __METHOD__
);
883 return $this->revision
;
887 * @return RenderedRevision
889 public function getRenderedRevision() {
890 $this->assertPrepared( __METHOD__
);
892 return $this->renderedRevision
;
895 private function assertHasPageState( $method ) {
896 if ( !$this->pageState
) {
897 throw new LogicException(
898 'Must call grabCurrentRevision() or prepareContent() '
899 . 'or prepareUpdate() before calling ' . $method
904 private function assertPrepared( $method ) {
905 if ( !$this->revision
) {
906 throw new LogicException(
907 'Must call prepareContent() or prepareUpdate() before calling ' . $method
912 private function assertHasRevision( $method ) {
913 if ( !$this->revision
->getId() ) {
914 throw new LogicException(
915 'Must call prepareUpdate() before calling ' . $method
921 * Whether the edit creates the page.
925 public function isCreation() {
926 $this->assertPrepared( __METHOD__
);
927 return $this->options
['created'];
931 * Whether the edit created, or should create, a new revision (that is, it's not a null-edit).
933 * @warning at present, "null-revisions" that do not change content but do have a revision
934 * record would return false after prepareContent(), but true after prepareUpdate()!
935 * This should probably be fixed.
939 public function isChange() {
940 $this->assertPrepared( __METHOD__
);
941 return $this->options
['changed'];
945 * Whether the page was a redirect before the edit.
949 public function wasRedirect() {
950 $this->assertHasPageState( __METHOD__
);
952 if ( $this->pageState
['oldIsRedirect'] === null ) {
953 /** @var RevisionRecord $rev */
954 $rev = $this->pageState
['oldRevision'];
956 $this->pageState
['oldIsRedirect'] = $this->revisionIsRedirect( $rev );
958 $this->pageState
['oldIsRedirect'] = false;
962 return $this->pageState
['oldIsRedirect'];
966 * Returns the slots of the target revision, after PST.
968 * @note Callers must treat the returned RevisionSlots instance as immutable, even
969 * if it is a MutableRevisionSlots instance.
971 * @return RevisionSlots
973 public function getSlots() {
974 $this->assertPrepared( __METHOD__
);
975 return $this->revision
->getSlots();
979 * Returns the RevisionSlotsUpdate for this updater.
981 * @return RevisionSlotsUpdate
983 private function getRevisionSlotsUpdate() {
984 $this->assertPrepared( __METHOD__
);
986 if ( !$this->slotsUpdate
) {
987 $old = $this->getParentRevision();
988 $this->slotsUpdate
= RevisionSlotsUpdate
::newFromRevisionSlots(
989 $this->revision
->getSlots(),
990 $old ?
$old->getSlots() : null
993 return $this->slotsUpdate
;
997 * Returns the role names of the slots touched by the new revision,
998 * including removed roles.
1002 public function getTouchedSlotRoles() {
1003 return $this->getRevisionSlotsUpdate()->getTouchedRoles();
1007 * Returns the role names of the slots modified by the new revision,
1008 * not including removed roles.
1012 public function getModifiedSlotRoles() {
1013 return $this->getRevisionSlotsUpdate()->getModifiedRoles();
1017 * Returns the role names of the slots removed by the new revision.
1021 public function getRemovedSlotRoles() {
1022 return $this->getRevisionSlotsUpdate()->getRemovedRoles();
1026 * Prepare derived data updates targeting the given Revision.
1028 * Calling this method requires the given revision to be present in the database.
1029 * This may be right after a new revision has been created, or when re-generating
1030 * derived data e.g. in ApiPurge, RefreshLinksJob, and the refreshLinks
1033 * @see docs/pageupdater.txt for more information on when thie method can and should be called.
1035 * @note Calling this method more than once with the same revision has no effect.
1036 * $options are only used for the first call. Calling this method multiple times with
1037 * different revisions will cause an exception.
1039 * @note If grabCurrentRevision() (or prepareContent()) has been called before
1040 * calling this method, $revision->getParentRevision() has to refer to the revision that
1041 * was the current revision at the time grabCurrentRevision() was called.
1043 * @param RevisionRecord $revision
1044 * @param array $options Array of options, following indexes are used:
1045 * - changed: bool, whether the revision changed the content (default true)
1046 * - created: bool, whether the revision created the page (default false)
1047 * - moved: bool, whether the page was moved (default false)
1048 * - restored: bool, whether the page was undeleted (default false)
1049 * - oldrevision: Revision object for the pre-update revision (default null)
1050 * - triggeringUser: The user triggering the update (UserIdentity, defaults to the
1051 * user who created the revision)
1052 * - oldredirect: bool, null, or string 'no-change' (default null):
1053 * - bool: whether the page was counted as a redirect before that
1054 * revision, only used in changed is true and created is false
1055 * - null or 'no-change': don't update the redirect status.
1056 * - oldcountable: bool, null, or string 'no-change' (default null):
1057 * - bool: whether the page was counted as an article before that
1058 * revision, only used in changed is true and created is false
1059 * - null: if created is false, don't update the article count; if created
1060 * is true, do update the article count
1061 * - 'no-change': don't update the article count, ever
1062 * When set to null, pageState['oldCountable'] will be used instead if available.
1063 * - causeAction: an arbitrary string identifying the reason for the update.
1064 * See DataUpdate::getCauseAction(). (default 'unknown')
1065 * - causeAgent: name of the user who caused the update. See DataUpdate::getCauseAgent().
1066 * (string, default 'unknown')
1068 public function prepareUpdate( RevisionRecord
$revision, array $options = [] ) {
1070 !isset( $options['oldrevision'] )
1071 ||
$options['oldrevision'] instanceof Revision
1072 ||
$options['oldrevision'] instanceof RevisionRecord
,
1073 '$options["oldrevision"]',
1074 'must be a RevisionRecord (or Revision)'
1077 !isset( $options['triggeringUser'] )
1078 ||
$options['triggeringUser'] instanceof UserIdentity
,
1079 '$options["triggeringUser"]',
1080 'must be a UserIdentity'
1083 if ( !$revision->getId() ) {
1084 throw new InvalidArgumentException(
1085 'Revision must have an ID set for it to be used with prepareUpdate()!'
1089 if ( $this->revision
&& $this->revision
->getId() ) {
1090 if ( $this->revision
->getId() === $revision->getId() ) {
1091 return; // nothing to do!
1093 throw new LogicException(
1094 'Trying to re-use DerivedPageDataUpdater with revision '
1095 . $revision->getId()
1096 . ', but it\'s already bound to revision '
1097 . $this->revision
->getId()
1102 if ( $this->revision
1103 && !$this->revision
->getSlots()->hasSameContent( $revision->getSlots() )
1105 throw new LogicException(
1106 'The Revision provided has mismatching content!'
1110 // Override fields defined in $this->options with values from $options.
1111 $this->options
= array_intersect_key( $options, $this->options
) +
$this->options
;
1113 if ( isset( $this->pageState
['oldId'] ) ) {
1114 $oldId = $this->pageState
['oldId'];
1115 } elseif ( isset( $this->options
['oldrevision'] ) ) {
1116 /** @var Revision|RevisionRecord $oldRev */
1117 $oldRev = $this->options
['oldrevision'];
1118 $oldId = $oldRev->getId();
1120 $oldId = $revision->getParentId();
1123 if ( $oldId !== null ) {
1124 // XXX: what if $options['changed'] disagrees?
1125 // MovePage creates a dummy revision with changed = false!
1126 // We may want to explicitly distinguish between "no new revision" (null-edit)
1127 // and "new revision without new content" (dummy revision).
1129 if ( $oldId === $revision->getParentId() ) {
1130 // NOTE: this may still be a NullRevision!
1132 $this->options
['changed'] = true;
1133 } elseif ( $oldId === $revision->getId() ) {
1135 $this->options
['changed'] = false;
1137 // This indicates that calling code has given us the wrong Revision object
1138 throw new LogicException(
1139 'The Revision mismatches old revision ID: '
1140 . 'Old ID is ' . $oldId
1141 . ', parent ID is ' . $revision->getParentId()
1142 . ', revision ID is ' . $revision->getId()
1147 // If prepareContent() was used to generate the PST content (which is indicated by
1148 // $this->slotsUpdate being set), and this is not a null-edit, then the given
1149 // revision must have the acting user as the revision author. Otherwise, user
1150 // signatures generated by PST would mismatch the user in the revision record.
1151 if ( $this->user
!== null && $this->options
['changed'] && $this->slotsUpdate
) {
1152 $user = $revision->getUser();
1153 if ( !$this->user
->equals( $user ) ) {
1154 throw new LogicException(
1155 'The Revision provided has a mismatching actor: expected '
1156 . $this->user
->getName()
1163 // If $this->pageState was not yet initialized by grabCurrentRevision or prepareContent,
1164 // emulate the state of the page table before the edit, as good as we can.
1165 if ( !$this->pageState
) {
1166 $this->pageState
= [
1167 'oldIsRedirect' => isset( $this->options
['oldredirect'] )
1168 && is_bool( $this->options
['oldredirect'] )
1169 ?
$this->options
['oldredirect']
1171 'oldCountable' => isset( $this->options
['oldcountable'] )
1172 && is_bool( $this->options
['oldcountable'] )
1173 ?
$this->options
['oldcountable']
1177 if ( $this->options
['changed'] ) {
1178 // The edit created a new revision
1179 $this->pageState
['oldId'] = $revision->getParentId();
1181 if ( isset( $this->options
['oldrevision'] ) ) {
1182 $rev = $this->options
['oldrevision'];
1183 $this->pageState
['oldRevision'] = $rev instanceof Revision
1184 ?
$rev->getRevisionRecord()
1188 // This is a null-edit, so the old revision IS the new revision!
1189 $this->pageState
['oldId'] = $revision->getId();
1190 $this->pageState
['oldRevision'] = $revision;
1194 // "created" is forced here
1195 $this->options
['created'] = ( $this->pageState
['oldId'] === 0 );
1197 $this->revision
= $revision;
1199 $this->doTransition( 'has-revision' );
1201 // NOTE: in case we have a User object, don't override with a UserIdentity.
1202 // We already checked that $revision->getUser() mathces $this->user;
1203 if ( !$this->user
) {
1204 $this->user
= $revision->getUser( RevisionRecord
::RAW
);
1207 // Prune any output that depends on the revision ID.
1208 if ( $this->renderedRevision
) {
1209 $this->renderedRevision
->updateRevision( $revision );
1212 // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
1213 // NOTE: the revision is either new or current, so we can bypass audience checks.
1214 $this->renderedRevision
= $this->revisionRenderer
->getRenderedRevision(
1218 [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord
::RAW
]
1221 // XXX: Since we presumably are dealing with the current revision,
1222 // we could try to get the ParserOutput from the parser cache.
1225 // TODO: optionally get ParserOutput from the ParserCache here.
1226 // Move the logic used by RefreshLinksJob here!
1230 * @deprecated This only exists for B/C, use the getters on DerivedPageDataUpdater directly!
1231 * @return PreparedEdit
1233 public function getPreparedEdit() {
1234 $this->assertPrepared( __METHOD__
);
1236 $slotsUpdate = $this->getRevisionSlotsUpdate();
1237 $preparedEdit = new PreparedEdit();
1239 $preparedEdit->popts
= $this->getCanonicalParserOptions();
1240 $preparedEdit->output
= $this->getCanonicalParserOutput();
1241 $preparedEdit->pstContent
= $this->revision
->getContent( SlotRecord
::MAIN
);
1242 $preparedEdit->newContent
=
1243 $slotsUpdate->isModifiedSlot( SlotRecord
::MAIN
)
1244 ?
$slotsUpdate->getModifiedSlot( SlotRecord
::MAIN
)->getContent()
1245 : $this->revision
->getContent( SlotRecord
::MAIN
); // XXX: can we just remove this?
1246 $preparedEdit->oldContent
= null; // unused. // XXX: could get this from the parent revision
1247 $preparedEdit->revid
= $this->revision ?
$this->revision
->getId() : null;
1248 $preparedEdit->timestamp
= $preparedEdit->output
->getCacheTime();
1249 $preparedEdit->format
= $preparedEdit->pstContent
->getDefaultFormat();
1251 return $preparedEdit;
1255 * @param string $role
1256 * @param bool $generateHtml
1257 * @return ParserOutput
1259 public function getSlotParserOutput( $role, $generateHtml = true ) {
1260 return $this->getRenderedRevision()->getSlotParserOutput(
1262 [ 'generate-html' => $generateHtml ]
1267 * @return ParserOutput
1269 public function getCanonicalParserOutput() {
1270 return $this->getRenderedRevision()->getRevisionParserOutput();
1274 * @return ParserOptions
1276 public function getCanonicalParserOptions() {
1277 return $this->getRenderedRevision()->getOptions();
1281 * @param bool $recursive
1283 * @return DeferrableUpdate[]
1285 public function getSecondaryDataUpdates( $recursive = false ) {
1286 if ( $this->isContentDeleted() ) {
1287 // This shouldn't happen, since the current content is always public,
1288 // and DataUpates are only needed for current content.
1292 $output = $this->getCanonicalParserOutput();
1294 // Construct a LinksUpdate for the combined canonical output.
1295 $linksUpdate = new LinksUpdate(
1301 $allUpdates = [ $linksUpdate ];
1303 // NOTE: Run updates for all slots, not just the modified slots! Otherwise,
1304 // info for an inherited slot may end up being removed. This is also needed
1305 // to ensure that purges are effective.
1306 $renderedRevision = $this->getRenderedRevision();
1307 foreach ( $this->getSlots()->getSlotRoles() as $role ) {
1308 $slot = $this->getRawSlot( $role );
1309 $content = $slot->getContent();
1310 $handler = $content->getContentHandler();
1312 $updates = $handler->getSecondaryDataUpdates(
1318 $allUpdates = array_merge( $allUpdates, $updates );
1320 // TODO: remove B/C hack in 1.32!
1321 // NOTE: we assume that the combined output contains all relevant meta-data for
1323 $legacyUpdates = $content->getSecondaryDataUpdates(
1330 // HACK: filter out redundant and incomplete LinksUpdates
1331 $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1332 return !( $update instanceof LinksUpdate
);
1335 $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1338 // XXX: if a slot was removed by an earlier edit, but deletion updates failed to run at
1339 // that time, we don't know for which slots to run deletion updates when purging a page.
1340 // We'd have to examine the entire history of the page to determine that. Perhaps there
1341 // could be a "try extra hard" mode for that case that would run a DB query to find all
1342 // roles/models ever used on the page. On the other hand, removing slots should be quite
1343 // rare, so perhaps this isn't worth the trouble.
1345 // TODO: consolidate with similar logic in WikiPage::getDeletionUpdates()
1346 $wikiPage = $this->getWikiPage();
1347 $parentRevision = $this->getParentRevision();
1348 foreach ( $this->getRemovedSlotRoles() as $role ) {
1349 // HACK: we should get the content model of the removed slot from a SlotRoleHandler!
1350 // For now, find the slot in the parent revision - if the slot was removed, it should
1351 // always exist in the parent revision.
1352 $parentSlot = $parentRevision->getSlot( $role, RevisionRecord
::RAW
);
1353 $content = $parentSlot->getContent();
1354 $handler = $content->getContentHandler();
1356 $updates = $handler->getDeletionUpdates(
1360 $allUpdates = array_merge( $allUpdates, $updates );
1362 // TODO: remove B/C hack in 1.32!
1363 $legacyUpdates = $content->getDeletionUpdates( $wikiPage );
1365 // HACK: filter out redundant and incomplete LinksDeletionUpdate
1366 $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1367 return !( $update instanceof LinksDeletionUpdate
);
1370 $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1373 // TODO: hard deprecate SecondaryDataUpdates in favor of RevisionDataUpdates in 1.33!
1375 'RevisionDataUpdates',
1376 [ $this->getTitle(), $renderedRevision, &$allUpdates ]
1383 * Do standard updates after page edit, purge, or import.
1384 * Update links tables, site stats, search index, title cache, message cache, etc.
1385 * Purges pages that depend on this page when appropriate.
1386 * With a 10% chance, triggers pruning the recent changes table.
1388 * @note prepareUpdate() must be called before calling this method!
1390 * MCR migration note: this replaces WikiPage::doEditUpdates.
1392 public function doUpdates() {
1393 $this->assertTransition( 'done' );
1395 // TODO: move logic into a PageEventEmitter service
1397 $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
1399 $legacyUser = User
::newFromIdentity( $this->user
);
1400 $legacyRevision = new Revision( $this->revision
);
1402 $this->doParserCacheUpdate();
1404 $this->doSecondaryDataUpdates( [
1405 // T52785 do not update any other pages on a null edit
1406 'recursive' => $this->options
['changed'],
1407 'defer' => DeferredUpdates
::POSTSEND
,
1410 // TODO: MCR: check if *any* changed slot supports categories!
1411 if ( $this->rcWatchCategoryMembership
1412 && $this->getContentHandler( SlotRecord
::MAIN
)->supportsCategories() === true
1413 && ( $this->options
['changed'] ||
$this->options
['created'] )
1414 && !$this->options
['restored']
1416 // Note: jobs are pushed after deferred updates, so the job should be able to see
1417 // the recent change entry (also done via deferred updates) and carry over any
1418 // bot/deletion/IP flags, ect.
1419 $this->jobQueueGroup
->lazyPush(
1420 CategoryMembershipChangeJob
::newSpec(
1422 $this->revision
->getTimestamp()
1427 // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1428 $editInfo = $this->getPreparedEdit();
1429 Hooks
::run( 'ArticleEditUpdates', [ &$wikiPage, &$editInfo, $this->options
['changed'] ] );
1431 // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1432 if ( Hooks
::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
1433 // Flush old entries from the `recentchanges` table
1434 if ( mt_rand( 0, 9 ) == 0 ) {
1435 $this->jobQueueGroup
->lazyPush( RecentChangesUpdateJob
::newPurgeJob() );
1439 $id = $this->getPageId();
1440 $title = $this->getTitle();
1441 $dbKey = $title->getPrefixedDBkey();
1442 $shortTitle = $title->getDBkey();
1444 if ( !$title->exists() ) {
1445 wfDebug( __METHOD__
. ": Page doesn't exist any more, bailing out\n" );
1447 $this->doTransition( 'done' );
1451 if ( $this->options
['oldcountable'] === 'no-change' ||
1452 ( !$this->options
['changed'] && !$this->options
['moved'] )
1455 } elseif ( $this->options
['created'] ) {
1456 $good = (int)$this->isCountable();
1457 } elseif ( $this->options
['oldcountable'] !== null ) {
1458 $good = (int)$this->isCountable()
1459 - (int)$this->options
['oldcountable'];
1460 } elseif ( isset( $this->pageState
['oldCountable'] ) ) {
1461 $good = (int)$this->isCountable()
1462 - (int)$this->pageState
['oldCountable'];
1466 $edits = $this->options
['changed'] ?
1 : 0;
1467 $pages = $this->options
['created'] ?
1 : 0;
1469 DeferredUpdates
::addUpdate( SiteStatsUpdate
::factory(
1470 [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
1473 // TODO: make search infrastructure aware of slots!
1474 $mainSlot = $this->revision
->getSlot( SlotRecord
::MAIN
);
1475 if ( !$mainSlot->isInherited() && !$this->isContentDeleted() ) {
1476 DeferredUpdates
::addUpdate( new SearchUpdate( $id, $dbKey, $mainSlot->getContent() ) );
1479 // If this is another user's talk page, update newtalk.
1480 // Don't do this if $options['changed'] = false (null-edits) nor if
1481 // it's a minor edit and the user making the edit doesn't generate notifications for those.
1482 if ( $this->options
['changed']
1483 && $title->getNamespace() == NS_USER_TALK
1484 && $shortTitle != $legacyUser->getTitleKey()
1485 && !( $this->revision
->isMinor() && $legacyUser->isAllowed( 'nominornewtalk' ) )
1487 $recipient = User
::newFromName( $shortTitle, false );
1488 if ( !$recipient ) {
1489 wfDebug( __METHOD__
. ": invalid username\n" );
1491 // Allow extensions to prevent user notification
1492 // when a new message is added to their talk page
1493 // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1494 if ( Hooks
::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
1495 if ( User
::isIP( $shortTitle ) ) {
1496 // An anonymous user
1497 $recipient->setNewtalk( true, $legacyRevision );
1498 } elseif ( $recipient->isLoggedIn() ) {
1499 $recipient->setNewtalk( true, $legacyRevision );
1501 wfDebug( __METHOD__
. ": don't need to notify a nonexistent user\n" );
1507 if ( $title->getNamespace() == NS_MEDIAWIKI
1508 && $this->getRevisionSlotsUpdate()->isModifiedSlot( SlotRecord
::MAIN
)
1510 $mainContent = $this->isContentDeleted() ?
null : $this->getRawContent( SlotRecord
::MAIN
);
1512 $this->messageCache
->updateMessageOverride( $title, $mainContent );
1515 // TODO: move onArticleCreate and onArticle into a PageEventEmitter service
1516 if ( $this->options
['created'] ) {
1517 WikiPage
::onArticleCreate( $title );
1518 } elseif ( $this->options
['changed'] ) { // T52785
1519 WikiPage
::onArticleEdit( $title, $legacyRevision, $this->getTouchedSlotRoles() );
1522 $oldRevision = $this->getParentRevision();
1523 $oldLegacyRevision = $oldRevision ?
new Revision( $oldRevision ) : null;
1525 // TODO: In the wiring, register a listener for this on the new PageEventEmitter
1526 ResourceLoaderWikiModule
::invalidateModuleCache(
1527 $title, $oldLegacyRevision, $legacyRevision, $this->getWikiId() ?
: wfWikiID()
1530 $this->doTransition( 'done' );
1534 * Do secondary data updates (such as updating link tables).
1536 * MCR note: this method is temporarily exposed via WikiPage::doSecondaryDataUpdates.
1538 * @param array $options
1539 * - recursive: make the update recursive, i.e. also update pages which transclude the
1540 * current page or otherwise depend on it (default: false)
1541 * - defer: one of the DeferredUpdates constants, or false to run immediately after waiting
1542 * for replication of the changes from the SecondaryDataUpdates hooks (default: false)
1543 * - transactionTicket: a transaction ticket from LBFactory::getEmptyTransactionTicket(),
1544 * only when defer is false (default: null)
1547 public function doSecondaryDataUpdates( array $options = [] ) {
1548 $this->assertHasRevision( __METHOD__
);
1550 'recursive' => false,
1552 'transactionTicket' => null,
1554 $deferValues = [ false, DeferredUpdates
::PRESEND
, DeferredUpdates
::POSTSEND
];
1555 if ( !in_array( $options['defer'], $deferValues, true ) ) {
1556 throw new InvalidArgumentException( 'invalid value for defer: ' . $options['defer'] );
1558 Assert
::parameterType( 'integer|null', $options['transactionTicket'],
1559 '$options[\'transactionTicket\']' );
1561 $updates = $this->getSecondaryDataUpdates( $options['recursive'] );
1563 $triggeringUser = $this->options
['triggeringUser'] ??
$this->user
;
1564 if ( !$triggeringUser instanceof User
) {
1565 $triggeringUser = User
::newFromIdentity( $triggeringUser );
1567 $causeAction = $this->options
['causeAction'] ??
'unknown';
1568 $causeAgent = $this->options
['causeAgent'] ??
'unknown';
1569 $legacyRevision = new Revision( $this->revision
);
1571 if ( $options['defer'] === false && $options['transactionTicket'] !== null ) {
1572 // For legacy hook handlers doing updates via LinksUpdateConstructed, make sure
1573 // any pending writes they made get flushed before the doUpdate() calls below.
1574 // This avoids snapshot-clearing errors in LinksUpdate::acquirePageLock().
1575 $this->loadbalancerFactory
->commitAndWaitForReplication(
1576 __METHOD__
, $options['transactionTicket']
1580 foreach ( $updates as $update ) {
1581 if ( $update instanceof DataUpdate
) {
1582 $update->setCause( $causeAction, $causeAgent );
1584 if ( $update instanceof LinksUpdate
) {
1585 $update->setRevision( $legacyRevision );
1586 $update->setTriggeringUser( $triggeringUser );
1588 if ( $options['defer'] === false ) {
1589 if ( $options['transactionTicket'] !== null ) {
1590 $update->setTransactionTicket( $options['transactionTicket'] );
1592 $update->doUpdate();
1594 DeferredUpdates
::addUpdate( $update, $options['defer'] );
1599 public function doParserCacheUpdate() {
1600 $this->assertHasRevision( __METHOD__
);
1602 $wikiPage = $this->getWikiPage(); // TODO: ParserCache should accept a RevisionRecord instead
1604 // NOTE: this may trigger the first parsing of the new content after an edit (when not
1605 // using pre-generated stashed output).
1606 // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
1607 // to be performed post-send. The client could already follow a HTTP redirect to the
1608 // page view, but would then have to wait for a response until rendering is complete.
1609 $output = $this->getCanonicalParserOutput();
1611 // Save it to the parser cache. Use the revision timestamp in the case of a
1612 // freshly saved edit, as that matches page_touched and a mismatch would trigger an
1613 // unnecessary reparse.
1614 $timestamp = $this->options
['changed'] ?
$this->revision
->getTimestamp()
1615 : $output->getTimestamp();
1616 $this->parserCache
->save(
1617 $output, $wikiPage, $this->getCanonicalParserOptions(),
1618 $timestamp, $this->revision
->getId()